package idv.takeshi.software.productline.bookstore.domain.model.category;

import idv.takeshi.software.productline.bookstore.domain.model.valueobject.InternationalText;
import idv.takeshi.software.productline.bookstore.domain.model.book.Book;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

/**
 * A model class represents as a category for book.
 * @author takeshi
 *
 */
@Entity
@NamedQueries(
		{
			@NamedQuery(name = "findByRoot", query = "SELECT c FROM Category c WHERE c.root = true")}
	)
public class Category {
	
	/**
	 * A comparator for sorting Books descending by their sold quantity. 
	 */
	private static final Comparator<Book> soldQuantityComparator = new Comparator<Book>(){

		
		public int compare(Book book1, Book book2) {
			int result = 0;
			result = -((int) (book1.getInventory().getIncrementalSoldQuantity() - 
					book2.getInventory().getIncrementalSoldQuantity()));
			result = result == 0? 1: result;
			return result;
		}
	};
	
	/**
	 * A comparator for sorting Books descending by their publish date. 
	 */
	private static final Comparator<Book> publishDateComparator = new Comparator<Book>(){

		
		public int compare(Book book1, Book book2) {
			int result = 0;
			result = -(book1.getPublishDate().compareTo( book2.getPublishDate()));
			result = result == 0? 1: result;
			return result;
		}
	};
	
	@Id
	@GeneratedValue(strategy = GenerationType.TABLE)
	private Long id;
	
	@Embedded
	private InternationalText name;
	
	@OneToMany(cascade = CascadeType.ALL)
	@JoinColumn(name = "PARENT_CATEGORY_ID")
	@OrderBy("sequence ASC")
	private Set<Category> subCategories = new LinkedHashSet<Category>();
	 
	@OneToMany
	@JoinColumn(name = "CATEGORY_ID")
	private Set<Book> books = new LinkedHashSet<Book>();
	
	/**
	 * Is a root category ? 
	 */
	@Column(name = "IS_ROOT")
	private boolean root;
	
	@Column(nullable = false)
	private int sequence;
	
	 
	/**
	 * Test whether any book contained.
	 * @return
	 */
	public boolean hasBooks() {
		return books.size() > 0;
	}
	 
	/**
	 * Test whether any sub-category contained.
	 * @return
	 */
	public boolean hasSubCategories() {
		return subCategories.size() > 0;
	}
	 
	/**
	 * Get unmodifiable books.
	 * @return
	 */
	public Set<Book> getBooks() {
		return Collections.unmodifiableSet(books);
	}
	
	/**
	 * Get a book from all books by id.
	 * @param id
	 * @return
	 */
	public Book getAllBook(Long id){
		Validate.notNull(id, "id shall always not be null");
		Book book = null;
		for(Book tmp : this.getAllBooks()){
			if(tmp.getId().equals(id)){
				book = tmp;
				break;
			}
		}
		return book;
	}
	
	/**
	 * Get unmodifiable sub-categories.
	 * @return
	 */
	public Set<Category> getSubCategories() {
		return Collections.unmodifiableSet(subCategories);
	}
	
	/**
	 * Get a category from all subCategory by id.
	 * @param id
	 * @return
	 */
	public Category getAllSubCategory(Long id){
		Validate.notNull(id, "id shall always not be null");
		Category category = null;
		for(Category tmp : this.getAllSubCategories()){
			if(tmp.getId().equals(id)){
				category = tmp;
				break;
			}
		}
		return category;
	}
	 
	/**
	 * Add a new book into this category.
	 * @param book
	 * @throws SubCategoryAlreadyAttachedException 
	 */
	public void addBook(Book book) throws SubCategoryAlreadyAttachedException {
		Validate.notNull(book, "book shall always not be null");
		if(this.subCategories.size() > 0)
			throw new SubCategoryAlreadyAttachedException(
					"sub-categories contained in this category already, while adding book:" + book);
		this.books.add(book);
	}
	 
	/**
	 * Add a new sub-category into this category.
	 * @param subCategory
	 * @throws BookAlreadyAttachedException 
	 */
	public void addSubCategory(Category subCategory) throws BookAlreadyAttachedException {
		Validate.notNull(subCategory, "subCategory shall always not be null");
		checkBooksSize(subCategory);
		
		//if contains, do nothing
		if(this.subCategories.contains(subCategory)) return;
		
		if(this.subCategories.size() > 0){
			subCategory.setSequence(this.subCategories.size() + 1);
		} else{
			subCategory.setSequence(1);
		}
		
		this.subCategories.add(subCategory);
	}

	/**
	 * @param subCategory
	 * @throws BookAlreadyAttachedException
	 */
	private void checkBooksSize(Category subCategory)
			throws BookAlreadyAttachedException {
		if(this.books.size() > 0)
			throw new BookAlreadyAttachedException(
					"books contained in this category already, while adding subCategory:" + subCategory);
	}
	
	/**
	 * Add/set a category and also specify this category's sequence.
	 * @param category
	 * @param sequence
	 * @throws BookAlreadyAttachedException
	 */
	public void addSubCategory(Category subCategory, int sequence) throws BookAlreadyAttachedException	{
		Validate.notNull(subCategory, "subCategory shall always not be null");
		Validate.isTrue(sequence > 0, "sequence:" + sequence + " shall always greater than 0");
		checkBooksSize(subCategory);
		
		if(this.subCategories.contains(subCategory))
			this.subCategories.remove(subCategory);
		
		if(sequence > this.subCategories.size()){
			this.subCategories.add(subCategory);
		} else {
			List<Category> tmpSubCategories = new ArrayList<Category>(this.subCategories);
			tmpSubCategories.add(sequence - 1, subCategory);
			this.subCategories = new LinkedHashSet<Category>(tmpSubCategories);
		}
		//reorder
		int newSequence = 1;
		for(Category category : this.subCategories){
			category.setSequence(newSequence++);
		}
	}
	 
	/**
	 * Remove a book.
	 * @param book
	 */
	public void removeBook(Book book) {
		this.books.remove(book);
	}
	 
	/**
	 * Remove a sub-category.
	 * @param subCategory
	 */
	public void removeSubCategory(Category subCategory) {
		this.subCategories.remove(subCategory);
	}
	
	/**
	 * Get a path for super categories by given any child-category.
	 * @param category
	 * @return A set includes from this category to 
	 * the last super category of the given child category.
	 */
	public Set<Category> getSuperCategories(Category category){
		Validate.notNull(category, "category shall always not be null");
		LinkedList<Category> superCategories = new LinkedList<Category>();
		this.collectSuperCategories(superCategories, this, category);
		Collections.reverse(superCategories);
		return new LinkedHashSet<Category>(superCategories);
	}
	
	/**
	 * Get a path for super categories by given any child-book.
	 * @param book
	 * @return
	 */
	public Set<Category> getSuperCategories(Book book){
		Validate.notNull(book, "book shall always not be null");
		LinkedList<Category> superCategories = new LinkedList<Category>();
		collectSuperCategories(superCategories, this, book);
		if(superCategories.size() > 0)
			superCategories.add(this);
		Collections.reverse(superCategories);
		return new LinkedHashSet<Category>(superCategories);
	}
	
	/**
	 * Get a super-category of the given book.
	 * @param book
	 * @return
	 */
	public Category getSuperCategory(Book book){
		Set<Category> superCategories = this.getSuperCategories(book);
		Category category = null;
		if(null != superCategories && superCategories.size() > 0){
			category = (Category) CollectionUtils.get(superCategories, 
					(superCategories.size() - 1));
		}
		return category;
	}
	
	/**
	 * Get all books based on this category and it's top N best sellers.
	 * @param topN
	 * @return
	 */
	public Set<Book> getAllBestSellers(int topN) {
		return this.getAllBestSellers(1, topN);
	}
	 
	/**
	 * Get all books based on this category and it's top N newest publish date.
	 * @param topN
	 * @return
	 */
	public Set<Book> getAllNewestBooks(int topN) {
		return this.getAllNewestBooks(1, topN);
	}
	 
	/**
	 * Get all books based on this category and it's top N best sellers.
	 * @param fromTopN
	 * @param toTopN
	 * @return
	 */
	public Set<Book> getAllBestSellers(int fromTopN, int toTopN) {
		Validate.isTrue(fromTopN > 0, "fromTopN:" + fromTopN + " shall greater than 0");
		Validate.isTrue(fromTopN < toTopN, "toTopN:" + toTopN + " shall greater than fromTopN:" + 
				fromTopN);
		Set<Book> books1 = getAllBestSellers();
		List<Book> books2 = null;
		
		int tmpToTopN = toTopN > books1.size()? books1.size(): toTopN;
		int tmpFromTopN = fromTopN - 1;
		
		books2 = new ArrayList<Book>(books1).subList(tmpFromTopN, tmpToTopN);
		
//		return books1.subList(tmpFromTopN, tmpToTopN);
		return new LinkedHashSet<Book>(books2);
	}
	 
	/**
	 * Get all books based on this category and it's top N newest publish date.
	 * @param fromTopN
	 * @param toTopN
	 * @return
	 */
	public Set<Book> getAllNewestBooks(int fromTopN, int toTopN) {
		Validate.isTrue(fromTopN > 0, "fromTopN:" + fromTopN + " shall greater than 0");
		Validate.isTrue(fromTopN < toTopN, "toTopN:" + toTopN + " shall greater than fromTopN:" + 
				fromTopN);
		Set<Book> books1 = getAllNewestBooks();
		List<Book> books2 = null;
		int tmpToTopN = toTopN > books1.size()? books1.size(): toTopN;
		int tmpFromTopN = fromTopN - 1;
		
		books2 = new ArrayList<Book>(books1).subList(tmpFromTopN, tmpToTopN);
		
//		return books1.subList(tmpFromTopN, tmpToTopN);
		return new LinkedHashSet<Book>(books2);
	}
	 
	/**
	 * Get all books based on this category and it's best sellers.
	 * @return
	 */
	public Set<Book> getAllBestSellers() {
		Set<Book> books1 = null;
		Set<Book> books2 = null;
		books1 = this.getAllBooks();
//		Collections.sort(books, soldQuantityComparator);
		if(null != books1 && books1.size() > 0){
			books2 = new TreeSet<Book>(soldQuantityComparator);
			books2.addAll(books1);
		} else{
			books2 = books1;
		}
		
		return books2;
	}
	
	/**
	 * Get all books based on this category and it's newest publish date.
	 * @return
	 */
	public Set<Book> getAllNewestBooks() {
		Set<Book> books1 = null;
		Set<Book> books2 = null;
		books1 = this.getAllBooks();
//		Collections.sort(books1, publishDateComparator);
		if(null != books1 && books1.size() > 0){
			books2 = new TreeSet<Book>(publishDateComparator);
			books2.addAll(books1);
		} else{
			books2 = books1;
		}

		return books2;
	}

	/**
	 * @return the id
	 */
	public Long getId() {
		return id;
	}

	/**
	 * @param id the id to set
	 */
	public void setId(Long id) {
		this.id = id;
	}

	/**
	 * @return the name
	 */
	public InternationalText getName() {
		return name;
	}

	/**
	 * @param name the name to set
	 */
	public void setName(InternationalText name) {
		this.name = name;
	}

	/**
	 * @return the root
	 */
	public boolean isRoot() {
		return root;
	}

	/**
	 * @param root the root to set
	 */
	public void setRoot(boolean root) {
		this.root = root;
	}

	/**
	 * @return the sequence
	 */
	public int getSequence() {
		return sequence;
	}

	/**
	 * @param sequence the sequence to set
	 */
	public void setSequence(int sequence) {
		this.sequence = sequence;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == null) { return false; }
		   if (obj == this) { return true; }
		   if(!(obj instanceof Category)) {
		     return false;
		   }
		   Category rhs = (Category) obj;
		   return new EqualsBuilder()
			   .append(this.id, rhs.id)
			   .append(this.name, rhs.name)
			   .append(this.subCategories, rhs.subCategories)
			   .append(this.books, rhs.books)
			   .append(this.root, rhs.root)
			   .append(this.sequence, rhs.sequence)
		       .isEquals();
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return new HashCodeBuilder(13, 17).
	       append(this.id).
	       append(this.name).
	       append(this.subCategories).
	       append(this.books).
	       append(this.root).
	       append(this.sequence).
	       toHashCode();
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return ReflectionToStringBuilder.toString(this, ToStringStyle.MULTI_LINE_STYLE);
	}

	/**
	 * Get all descendant books.
	 * @return
	 */
	public Set<Book> getAllBooks(){
		Set<Book> books = null;
		if(hasBooks()){
			books = this.books;
		} else if(hasSubCategories()){
			books = new LinkedHashSet<Book>();
			for(Category category : this.subCategories){
				books.addAll(category.getAllBooks());
			}
		} else{
			books = new LinkedHashSet<Book>();
		}
		return books;
	}
	
	/**
	 * check whether has any descendant books.
	 * @return
	 */
	public boolean hasAllBooks(){
		return this.getAllBooks().size() > 0;
	}

	/**
	 * Get all descendant sub-categories, including self at index 0.
	 * @return
	 */
	public Set<Category> getAllSubCategories(){
		Set<Category> categories = new LinkedHashSet<Category>();
		categories.add(this);
		
		if(hasSubCategories()){
			for(Category category : this.subCategories){
				categories.addAll(category.getAllSubCategories());
			}
		}
		return categories;
	}

	/**
	 * A recursive method to collect all super categories for dest book.
	 * @param categories
	 * @param src
	 * @param dest
	 * @return
	 */
	private boolean collectSuperCategories(LinkedList<Category> categories, Category src, Book dest){
		boolean hit = false;
		if(src.hasBooks()){
			for(Book book : src.getBooks()){
				if(book.getId().equals(dest.getId())){
					hit = true;
					categories.add(src);
					break;
				}
			}
			
//			if(src.getBooks().contains(dest)){
//				hit = true;
//				categories.add(src);
//			}
		} else if(src.hasSubCategories()){
			for(Category category : src.getSubCategories()){
				hit = collectSuperCategories(categories, category, dest);
				if(hit){
					categories.add(category);
					break;
				}
			}
		}
		return hit;
	}

	/**
	 * A recursive method to collect all super categories for dest category.
	 * @param categories
	 * @param src
	 * @param dest
	 * @return
	 */
	private boolean collectSuperCategories(LinkedList<Category> categories, Category src, Category dest){
		boolean hit = false;
		
		if(src.equals(dest)){
			hit = true;
		} else{
			Set<Category> subs = src.getSubCategories();
			if(null != subs && subs.size() > 0){
				for(Category category : subs){
					hit = collectSuperCategories(categories, category, dest);
					if(hit){
						categories.add(src);
						break;
					}
				}
			}
		}
		return hit;
	}
	 
}
 
